Skip to content

fix(objectql): judge an authored action artifact-shipped by each holder's owner key, not by bare name - #33271

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/area-E2b-32990
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/area-E2b-32990

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #32990

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:启动后的宿主上,ObjectQLPlugin 绑定环境自写的脚本 action 时,原来只要有任何代码包发布了同名 action 就跳过;现在逐个读每个持有者的同名 action,按它所属的对象判断:只有某个包的同名 action 拥有该行所在对象(或该 action 不绑定对象)时才跳过。声明式 .custom override 也按自己对象上的那个包匹配,不再按名字取第一个包。
  • 为什么改:两个包在 account、contact 上各有 approve,环境在 lead 上自写 approve 时,fix(runtime): the bare action route serves the environment's own action on an object no holder of a refused name binds #32987 已让 /actions/lead/approve 服务环境自己的声明,但它的脚本体从未被绑定,调用答 not found。
  • 风险与回滚:只改 resync 的跳过判断;同对象上的环境行仍是持有者的定制(不绑定,持有者的 handler 照跑);不绑定对象的包 action 仍覆盖所有对象;停用的持有者仍占住其对象。新增的绑定都在环境坐标(metadata-service)上,不进入任何包的坐标。不改存储、schema、spec。回滚即 revert。
  • 先看:packages/objectql/src/plugin.ts(isArtifactShippedAction、新的 artifactActionsNamed、isDeclaredArtifactActionOverride 中 artifacts.find 一行)。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/plugin-authored-actions.test.ts -t 32990,预期 6 条全部通过。
  • 需要你做的:无,席位落地。

What changes

ObjectQLPlugin.resyncAuthoredActions binds runtime-authored script actions through the engine's default action runner, and skips a row a code package ships (the app plugin binds that package's handler under app:PACKAGE). The skip, isArtifactShippedAction, asked registry.getArtifactItem('action', NAME): any code package's artifact of the bare name, on any object, skipped the row.

  • isArtifactShippedAction now reads every code package's artifact of the name (artifactActionsNamed) and judges each by the object it owns, with the router's own predicate standaloneActionOwnsRoute: the row is artifact-shipped when an artifact owns the row's object, an object-less artifact owning every object. The embedded-in-a-packaged-object branch is unchanged.
  • artifactActionsNamed (new, private): the registry's first match by bare name (what one holder always answered) plus each holder's own artifact (packageScopedHoldersOf('action', NAME, null, { includeDisabledPackages: true }), then getArtifactItem prefer-local per holder), deduplicated. Disabled packages count: a reversible disable never moves a binding.
  • isDeclaredArtifactActionOverride: the base is the artifact on the row's own object among every holder's, not the first artifact by name. With one holder the answer is the same; with two, a declared override of the holder registered second is now recognised.
  • action-handler-coordinate.ts: one docblock sentence follows the new judgement. No code change there.

No change to packages/runtime, packages/metadata-protocol, packages/objectql/src/registry.ts or packages/spec.

FROM → TO

Composition: package A ships a script action approve on its object, package B one of the same name on its own, and the environment stores its own package-less script action approve on a third object. Readings are the engine dispatch on a booted kernel (ObjectKernel + ObjectQLPlugin, its kernel:ready resync).

# Case FROM (main e540baf8) TO Clause-② arm
1 two holders (A, B), environment row on an object neither binds, both registration orders dispatch there answers Action 'approve' on object 'OBJECT' not found the environment's body runs; A's and B's run on their own objects, unqualified and package-qualified widening
2 one holder (A), environment row on another object not found the environment's body runs; A's runs on its own widening
3 B.custom declared override of B's action, A registered first override not bound, B's body runs the override's body runs over B's handler widening
4 the same, B registered first the override's body runs unchanged none

Unchanged, pinned as CONTROLs: a package-less row on an object a holder's action owns stays that holder's customization (the holder's body runs), both orders; an object-less package action keeps a row on any other object unbound; a DISABLED holder registered after another still owns its object.

Row 2 is the one-holder case of the same defect: the declaration the route serves there is unchanged (the environment's own item, as action-package-addressing.test.ts #32365 case 1 pins for the plain read), and what moves is that its body now has a handler. Named here so the seat can confirm it reads within the epic's "one-holder behaviour unchanged" red line (resolution untouched; the dispatch gains a handler).

Tests

packages/objectql/src/plugin-authored-actions.test.ts: a new top-level describe [#32990] …, on a booted kernel (the file is this resync's harness). It boots ObjectKernel with ObjectQLPlugin, a row-store driver double and a test plugin standing in for the runtime's AppPlugin (installs the default action runner, registers each package through the manifest service during the cold boot and binds its handler under app:PACKAGE). Six cases: rows 1, 2 and 3/4 above (both registration orders where order matters), and the three CONTROLs.

Premise measured on the unmodified source (the pins committed first, d2848f11): case 1 answered Action 'approve' on object 'env_lead' not found; the override case answered ran beta with A registered first.

Ablations (each through scripts/ablation-replace.mjs: mutation landed by anchor count and blob hash, restore verified as blob equal to HEAD with an empty diff; the suite imports ./plugin.js from source, so no rebuild is involved):

Five ablations, each red
  1. the judgement by bare name (artifactActionsNamed(name).length > 0): 2 failed (two holders; one holder).
  2. the per-holder artifact check never matches (some(() => false)): 2 failed (the disabled-holder and the object-less CONTROLs).
  3. owner-key equality in place of standaloneActionOwnsRoute: 1 failed (the object-less CONTROL).
  4. the override's base read as the first artifact (artifacts[0]): 1 failed (the override of the second holder).
  5. nothing artifact-shipped (return false before both branches): 3 failed (the three CONTROLs, the same-object customization among them; that CONTROL is also held by the embedded-object branch, which is why ablation 2 leaves it green).

A first attempt at ablation 5 used a replacement that contained its own anchor; ablation-replace refused it (anchor count 1 to 1) and restored, so it ran nothing; it was re-run with a different anchor.

Acceptance notes

  • ObjectQL's arrivesAsDeclaredBase (packages/objectql/src/engine-actions.ts) reads getArtifactItem('action', NAME) by bare name the same way when a declared base's handler arrives under an armed override. Not measured, not changed here; noted for the seat.
  • isArtifactShippedHook stays name-only; ADR-0150 D4 names it as gaining the package, a separate card.
  • First-party count: no first-party composition has two packages holding one action name beside an environment script action of that name.

Verification

Head at open: ab806df2. Local readings so far: the dependency closure built (pnpm --filter '@objectstack/objectql^...' build, lock VERDICT command-exit 0); vitest run src/plugin-authored-actions.test.ts src/action-activation.test.ts: Test Files 2 passed (2), Tests 50 passed (50). The package's full test, its typecheck and the derived gate families follow in the os-dev-report comment on #32990; CI: see the check runs.

🤖 Generated with Claude Code

…gement per holder on a booted kernel

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyWLSMyXZGc6dwhCAu
…er's owner key, not by bare name

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyWLSMyXZGc6dwhCAu
@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 6 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/action-handler-coordinate.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/objects.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via ObjectQLPlugin (symbol, a top-level class))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via ObjectQLPlugin (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/action-handler-coordinate.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 21 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 24f2f4014d0c9bbd43ec6928942fa44b21c1dff6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 699b186e50e529a73081207e5a948d63fe60838f — the merge of head efef4782e2d3383e25e732b74136739929681d0e into base 24f2f4014d0c9bbd43ec6928942fa44b21c1dff6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 699b186e50e529a73081207e5a948d63fe60838f && git checkout 699b186e50e529a73081207e5a948d63fe60838f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 24f2f4014d0c9bbd43ec6928942fa44b21c1dff6 efef4782e2d3383e25e732b74136739929681d0e && git checkout -B drift-repro 24f2f4014d0c9bbd43ec6928942fa44b21c1dff6 && git merge --no-ff efef4782e2d3383e25e732b74136739929681d0e

node scripts/docs-audit/affected-docs.mjs --json 24f2f4014d0c9bbd43ec6928942fa44b21c1dff6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 24f2f4014d0c9bbd43ec6928942fa44b21c1dff6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head ab806df · 2026-11-14T15:25:21Z

review#3 (area-lease pilot #33059).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: ab806df240348e20541288f4a5b4d803d7304563
Local-runs: none

① Derived judgments

  • isArtifactShippedAction now skips an authored row only when some holder's artifact owns the row's object (standaloneActionOwnsRoute: same owner key, or an object-less artifact). Checked the edge the table does not list: an object-less environment row (owner key global) beside a holder's artifact on account is now bound under metadata-service; dispatch at /actions/account/approve still probes account first (the holder's handler), and only objects no holder owns fall through to the environment's global handler (actionHandlerObjectKeys). Correct under D4.
  • artifactActionsNamed: the bare-name first match plus each holder's own artifact (packageScopedHoldersOf(..., { includeDisabledPackages: true })), deduplicated by identity. Disabled holders keep their object. Matches the stated rule.
  • should-fix S1 — the override base is still picked by list order when two artifacts share the row's owner key. isDeclaredArtifactActionOverride takes artifacts.find((a) => standaloneActionOwnerKey(a) === objectKey). For two holders whose approve is object-less (both key global), find returns the first by registration/bare-name order, so a declared B.custom override of B's object-less action is judged against A (isServedOverrideRowOf(row, A, installed) → false) and stays unbound, B's body runs. That is row 3's defect in the object-less shape. Fix (small, fold here): among artifacts at the row's owner key, take the one whose _packageId equals action.overrides?.package when given; with several and no package named, do not pick (return false and say so once). Add a pin with two object-less holders in both registration orders.
  • Row 2 (one holder, environment row on another object: not found → the environment's body runs): resolution and the declaration the route serves are unchanged (#32365 case 1); only the missing handler is added. I read it as within the epic's "one-holder behaviour unchanged" red line (a dispatch that failed now runs the declared item); seat may confirm.
  • action-handler-coordinate.ts: docblock only, matches the code.

② Semver level

@objectstack/objectql: patch; Clause-②: yes (widening) is right (dispatches that answered not-found now run the declared body; no refusal added, no export/schema moves). Metadata-protocol: none holds.

③ Boundary flags

  • Author login objectstack-fleet[bot] (relay); area E2b. Hot shared file packages/objectql/src/plugin.ts (A4/E2b/E1 per amendment 6): confirm no other open PR edits isArtifactShippedAction / isDeclaredArtifactActionOverride before landing.
  • CI not complete when read; the ACCEPT re-reads every check at the landing head.

Implemented-by: claude/area-E2b-32990
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: FAIL

S1 (≤30 lines + pin) → delta review by review#3.

…mes among holders at its owner key

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyWLSMyXZGc6dwhCAu
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head efef478 · 2026-11-14T16:56:44Z

review#3.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: efef4782e2d3383e25e732b74136739929681d0e
Local-runs: none

Delta over my FAIL at ab806df240348e20541288f4a5b4d803d7304563 (comment 6879713494). The diff ab806df2...efef4782 has two commits and touches packages/objectql/src/plugin.ts and plugin-authored-actions.test.ts.

① Derived judgments

  • S1 fixed. isDeclaredArtifactActionOverride no longer takes artifacts.find(...) by list order. Among the artifacts at the row's owner key, it takes the one whose _packageId is the row's overrides.package. A lone holder at the key is taken as before, so one-holder behaviour is unchanged. With several holders and none named, it picks no base: the row is bound as no override, and a once-per-process warn names the holders and the fix ("name the base package in the row's overrides.package"). That matches the ADR-0150 red line "ambiguity reported, never guessed".
  • Pin. Two object-less holders (both keyed global), a declared override of B, in both registration orders: B runs the override and A runs its own body. Before the fix, the "B then A" order is the case that bound against the wrong base.
  • Test double. The booted-kernel row-store find now honours ast.limit, so the double no longer answers more rows than the caller bounded. That is a test-only change and it tightens fidelity.
  • A row naming a package that is not among several holders at its key gets no base and is reported. A row naming a package other than a lone holder is judged by isServedOverrideRowOf against that holder, as before.

② Semver level

Unchanged from the first record: @objectstack/objectql: patch, Clause-②: yes (widening). The new no-base outcome only arises with several holders at one key, which the first record already treated as the per-holder widening.

③ Boundary flags

  • CI at this head when read (16:58Z): 33 success, 16 skipped, 1 in progress, none red. The ACCEPT re-reads every check at the landing head.

Implemented-by: claude/area-E2b-32990
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyWLSMyXZGc6dwhCAu
ACCEPT · area E2b (area-lease pilot #33059) · 2026-11-14T17:01Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 17:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 5e29b7e Nov 14, 2026
52 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/area-E2b-32990 branch November 14, 2026 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants